package magnet.processor.instances.parser;

import com.squareup.javapoet.*;
import magnet.Classifier;
import magnet.Instance;
import magnet.Scope;
import magnet.processor.MagnetProcessorEnv;
import magnet.processor.common.AptUtils;
import magnet.processor.common.CompilationException;
import magnet.processor.common.KotlinMethodMetadata;
import magnet.processor.common.ValidationException;
import magnet.processor.instances.Cardinality;
import magnet.processor.instances.Expression;
import magnet.processor.instances.FactoryType;
import magnet.processor.instances.MethodParameter;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import java.util.AbstractMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public abstract class InstanceParser<E extends Element> {

    public static final String FACTORY_SUFFIX = "MagnetFactory";
    private final String CLASS_NULLABLE = ".Nullable";

    private MagnetProcessorEnv env;
    private boolean isTypeInheritanceEnforced;
    private ClassName scopeTypeName = ClassName.get(Scope.class);
    private ClassName listTypeName = ClassName.get(List.class);
    //TODO Java没有Lazy
//    private ClassName lazyTypeName = ClassName.get(Lazy.class);

    public InstanceParser(MagnetProcessorEnv env,boolean isTypeInheritanceEnforced) {
        this.env = env;
        this.isTypeInheritanceEnforced = isTypeInheritanceEnforced;
    }

    public List<FactoryType> parse(E element) throws Throwable {

        onBeforeParsing(element);

        ParserInstance<E> elementParserInstance = new ParserInstance<>();
        elementParserInstance.setElement(element);

        final AttributeParser.Scope<E>[] scope = new AttributeParser.Scope[]{new AttributeParser.Scope<E>(
                isTypeInheritanceEnforced,
                elementParserInstance,
                element,
                env)};

        AptUtils.eachAttributeOf(element, Instance.class, new AptUtils.EachAttributeOfBlock() {
            @Override
            public void block(String name, AnnotationValue value) throws ValidationException, CompilationException {
                Map<String, AttributeParser> PARSERS = AttributeParser.Registry.getInstance().PARSERS;
                AttributeParser attributeParser = PARSERS.get(name);
                if (attributeParser != null) {
                    AttributeParser.Scope copyScope = scope[0].copy();
                    copyScope.setInstance(attributeParser.parse(scope[0], value));
                    scope[0] = copyScope;
                } else {
                    throw new CompilationException(element, "Unsupported attribute '" + name + "'." +
                            " Do you use the same versions of magnet processor and runtime libraries?");
                }
            }
        });

        ParserInstance<E> instance = scope[0].getInstance();
        for (AspectValidator validator : AspectValidator.Registry.INSTANCE().VALIDATORS) {
            instance = validator.validate(instance, env);
        }

        return generateFactories(instance);
    }

    protected void onBeforeParsing(E element) throws ValidationException {

    }

    protected abstract List<FactoryType> generateFactories(ParserInstance<E> instance) throws ValidationException, CompilationException;

    protected MethodParameter parseMethodParameter(Element element, VariableElement variable,KotlinMethodMetadata methodMetadata) throws ValidationException {

        TypeMirror variableType = variable.asType();
        if (variableType.getKind() == TypeKind.TYPEVAR) {
            throw new ValidationException(element, "Constructor parameter '" + variable.getSimpleName() + "' is specified using a generic" +
                    " type which is not supported by Magnet. Use a non-parameterized class or" +
                    " interface type instead. To inject current scope into an instance," +
                    " add 'scope: Scope' to the constructor parameters.");
        }

        ParameterSpec paramSpec = ParameterSpec.get(variable);
        TypeName paramType = paramSpec.type;
        String paramName = paramSpec.name;

        final TypeName[] paramReturnType = {paramType};
        final Expression[] paramExpression = {Expression.Scope.INSTANCE()};
        final TypeName[] paramParameterType = {paramType};
        final String[] paramClassifier = {Classifier.NONE};
        final boolean[] paramTypeErased = {false};

        parseParamType(paramParameterType[0], paramName, methodMetadata, variable, new ParseParamTypeBlock() {
            @Override
            public void block(TypeName returnType, Expression expression, TypeName parameterType, String classifier, boolean typeErased) {
                paramReturnType[0] = returnType;
                paramExpression[0] = expression;
                paramParameterType[0] = parameterType;
                paramClassifier[0] = classifier;
                paramTypeErased[0] = typeErased;
            }
        });

        return new MethodParameter(
                paramName,
                paramExpression[0],
                paramReturnType[0],
                paramParameterType[0],
                paramClassifier[0],
                paramTypeErased[0]
        );
    }

    private void parseParamType(TypeName typeName, String paramName,KotlinMethodMetadata methodMeta,VariableElement variable,ParseParamTypeBlock block) throws ValidationException {

        TypeName paramReturnType = typeName;
        final Expression[] paramExpression = {Expression.Scope.INSTANCE()};
        TypeName paramParameterType = typeName;
        final String[] paramClassifier = {Classifier.NONE};
        boolean paramTypeErased = false;

        if (typeName.equals(scopeTypeName)) {
            //TODO 不确定此处转换是否正确
            paramExpression[0] = Expression.Scope.INSTANCE();
        } else if (typeName instanceof ParameterizedTypeName) {
            if (((ParameterizedTypeName) typeName).rawType.equals(listTypeName)) {
                AbstractMap.SimpleEntry<TypeName, Boolean> typeNameBooleanSimpleEntry = firstArgumentRawType((ParameterizedTypeName) typeName, variable);
                TypeName type = typeNameBooleanSimpleEntry.getKey();
                Boolean erased = typeNameBooleanSimpleEntry.getValue();
                paramParameterType = type;
                paramTypeErased = erased;
                if (erased) {
                    paramReturnType = listTypeName;
                } else {
                    paramReturnType = ParameterizedTypeName.get(listTypeName, paramParameterType);
                }
                paramExpression[0] = new Expression.Getter(Cardinality.Many);
                annotations(variable, new AnnotationsBlock() {
                    @Override
                    public void block(Cardinality cardinality, String classifier) {
                        paramClassifier[0] = classifier;
                    }
                });
            }
            //TODO Java版本屏蔽掉对Lazy的处理
//            else if (((ParameterizedTypeName) typeName).rawType.equals(lazyTypeName)){
//
//            }
            else {
                paramParameterType = ((ParameterizedTypeName) typeName).rawType;
                paramTypeErased = true;
                annotations(variable, new AnnotationsBlock() {
                    @Override
                    public void block(Cardinality cardinality, String classifier) {
                        paramExpression[0] = new Expression.Getter(cardinality);
                        paramClassifier[0] = classifier;
                    }
                });
            }
        } else {
            annotations(variable, new AnnotationsBlock() {
                @Override
                public void block(Cardinality cardinality, String classifier) {
                    paramExpression[0] = new Expression.Getter(cardinality);
                    paramClassifier[0] = classifier;
                }
            });
        }

        block.block(
                paramReturnType,
                paramExpression[0],
                paramParameterType,
                paramClassifier[0],
                paramTypeErased
        );
    }

    public interface ParseParamTypeBlock {
        void block(
                TypeName returnType,
                Expression expression,
                TypeName parameterType,
                String classifier,
                boolean typeErased
        );
    }

    private AbstractMap.SimpleEntry<TypeName, Boolean> firstArgumentRawType(ParameterizedTypeName typeName, Element element) throws ValidationException {
        if (typeName.typeArguments.size() > 1) {
            throw new ValidationException(element, "Magnet supports type parametrized with a single argument only.");
        }
        TypeName argumentType = typeName.typeArguments.get(0);
        if (argumentType instanceof ParameterizedTypeName) {
            return new AbstractMap.SimpleEntry<>(((ParameterizedTypeName) argumentType).rawType,true);
        } else if (argumentType instanceof WildcardTypeName) {
            return firstUpperBoundsRawType((WildcardTypeName) argumentType, element);
        } else {
            return new AbstractMap.SimpleEntry<>(argumentType, false);
        }
    }

    private AbstractMap.SimpleEntry<TypeName, Boolean> firstUpperBoundsRawType(WildcardTypeName typeName, Element element) throws ValidationException {
        checkBounds(typeName, element);
        TypeName type = typeName.upperBounds.get(0);
        if (type instanceof ParameterizedTypeName) {
            return new AbstractMap.SimpleEntry<>(((ParameterizedTypeName) type).rawType, true);
        } else if (type instanceof WildcardTypeName) {
            return firstUpperBoundsRawType((WildcardTypeName) type, element);
        } else {
            return new AbstractMap.SimpleEntry<>(type, false);
        }
    }

    private void checkBounds(WildcardTypeName typeName, Element element) throws ValidationException {
        if (typeName.lowerBounds.size() > 0) {
            throw new ValidationException(
                    element,
                    "Magnet supports single upper bounds class parameter only," +
                            " while lower bounds class parameter was found."
            );
        }

        if (typeName.upperBounds.size() > 1) {
            throw new ValidationException(
                    element,
                    "Magnet supports single upper bounds class parameter only," +
                            " for example List<"+typeName.upperBounds.get(0)+">"
            );
        }
    }

    public interface AnnotationsBlock {
        void block(Cardinality cardinality, String classifier);
    }

    private void annotations(VariableElement element, AnnotationsBlock block) {
        Cardinality cardinality = Cardinality.Single;
        String classifier = Classifier.NONE;
        List<? extends AnnotationMirror> annotationMirrors = element.getAnnotationMirrors();
        for (AnnotationMirror annotationMirror : annotationMirrors) {
            if (AptUtils.isOfAnnotationType(annotationMirror, Classifier.class)) {
                Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
                Iterator<? extends AnnotationValue> iterator = elementValues.values().iterator();
                if (iterator.hasNext()) {
                    String declaredClassifier = iterator.next().toString();
                    if (declaredClassifier != null) {
                        classifier = removeSurrounding(declaredClassifier, "\"", "\"");
                    }
                }
            } else {
                String annotationType = annotationMirror.getAnnotationType().toString();
                if (annotationType.endsWith(CLASS_NULLABLE)) {
                    cardinality = Cardinality.Optional;
                }
            }
        }
        block.block(cardinality, classifier);
    }

    private String removeSurrounding(String str,String start,String end) {
        boolean startWith = str.startsWith(start);
        boolean endWith = str.endsWith(end);
        int strLength = str.length();
        if (strLength >= start.length() + end.length() && startWith && endWith) {
            return str.substring(start.length(), strLength - end.length());
        }
        return str;
    }
}
